/**
 * $Id: HttpSessionProviderImpl.java 57 2008-11-07 09:21:02Z daniele.galdi $
 */
package it.edgeworks.dsession.impl.mysql;

import com.thoughtworks.xstream.XStream;
import it.edgeworks.dsession.api.HttpSessionProviderBase;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import javax.naming.Context;
import javax.naming.InitialContext;
import javax.naming.NamingException;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.http.HttpSession;
import javax.sql.DataSource;
import java.io.*;
import java.sql.*;
import java.util.Map;
import java.util.zip.GZIPInputStream;
import java.util.zip.GZIPOutputStream;

/**
 * @author Daniele Galdi
 * @version $Revision: 57 $
 */
public class HttpSessionProviderImpl extends HttpSessionProviderBase {

    /**
     * The data-source parameter to use
     */
    private static final String INIT_PARAM_DATA_SOURCE = "data-source";

    /**
     * The optiona context-data-source parameter to use
     */
    private static final String CONTEXT_PARAM_DATA_SOURCE = "context-data-source";

    /**
     * The logger
     */
    protected final Log logger = LogFactory.getLog(getClass());

    /**
     * The data source
     */
    private DataSource dataSource;

    /**
     * This thread cleans all dead sessions
     */
    private Thread sessionCleaner;

    public void init(FilterConfig config) throws ServletException {
        super.init(config);

        dataSource = getDataSource(config);

        checkDatabase();

        sessionCleaner = createThread();
        sessionCleaner.start();
    }

    public boolean loadSession(String sessionId, HttpSession session) {
        boolean validSession = false;

        Connection conn = null;
        PreparedStatement ps = null;
        ResultSet rs = null;
        try {
            conn = getConnection();

            ps = conn.prepareStatement(SQL.SELECT_SESSION);
            ps.setString(1, sessionId);

            rs = ps.executeQuery();
            if (rs.next()) {
                InputStream is = rs.getBinaryStream(1);

                is = new GZIPInputStream(is);

                XStream stream = new XStream();
                Map serialized = (Map) stream.fromXML(is);
                map2session(serialized, session);

                is.close();
                validSession = true;
            }
        } catch (Exception e) {
            logger.error("An errors occurs during session:" + sessionId + " reading", e);
        } finally {
            SQL.close(rs);
            SQL.close(ps);
            SQL.close(conn);
        }
        return validSession;
    }

    public void saveSession(String sessionId, HttpSession session) {
        long start = System.currentTimeMillis();
        XStream xstream = new XStream();

        Connection conn = null;
        PreparedStatement ps = null;
        ResultSet rs = null;
        boolean ok = false;
        try {
            conn = getConnection();
            conn.setAutoCommit(false);

            ps = conn.prepareStatement(SQL.DROP_SESSION);
            ps.setString(1, sessionId);
            ps.executeUpdate();
            SQL.close(ps);

            logger.debug("Session: " + sessionId + " cleared");

            Map<String, Object> toSerialize = session2Map(session);

            ByteArrayOutputStream buffer = new ByteArrayOutputStream();
            OutputStream os = new GZIPOutputStream(buffer);
            xstream.toXML(toSerialize, os);
            os.flush();
            os.close();

            ps = conn.prepareStatement(SQL.CREATE_SESSION);
            ps.setString(1, sessionId);
            ps.setBytes(2, buffer.toByteArray());
            ps.executeUpdate();

            logger.debug("sessionId:" + sessionId + " tooks " + (System.currentTimeMillis() - start) + " millis for saving.");

            ok = true;
        } catch (Exception e) {
            logger.error("An errors occurs during session:" + sessionId + " writing", e);
        } finally {
            SQL.close(rs);
            SQL.close(ps);
            SQL.commit(conn, ok);
            SQL.close(conn);
        }
    }

    public void destroy() {
        sessionCleaner.interrupt();
    }

    private void checkDatabase() {
        Connection conn = null;
        PreparedStatement ps = null;
        try {
            conn = getConnection();
            ps = conn.prepareStatement(SQL.CREATE_TABLE_SESSIONS);
            ps.executeUpdate();
            logger.info("Database checked");
        } catch (SQLException e) {
            logger.error("An errors occurs during mysql session schema creation", e);
        } finally {
            SQL.close(ps);
            SQL.close(conn);
        }
    }

    /**
     * @param config The configuration to use
     * @return The JDBC DataSource for this filter instance.
     * @throws javax.servlet.ServletException if an error occurs
     */
    protected DataSource getDataSource(FilterConfig config) throws ServletException {
        String dataSourceName = config.getInitParameter(INIT_PARAM_DATA_SOURCE);
        if (dataSourceName == null) {
            dataSourceName = config.getServletContext()
                    .getInitParameter(CONTEXT_PARAM_DATA_SOURCE);
        }
        if (dataSourceName == null) {
            throw new ServletException(
                    "JDBC data source not specified. Please set the '"
                            + CONTEXT_PARAM_DATA_SOURCE
                            + "' context-param or the '"
                            + INIT_PARAM_DATA_SOURCE + "' servlet init-param.");
        }

        try {
            Context initContext = new InitialContext();
            Context envContext = (Context) initContext.lookup("java:/comp/env");
            return (DataSource) envContext.lookup(dataSourceName);
        } catch (NamingException ne) {
            throw new ServletException(
                    "Error getting reference to data source.", ne);
        }
    }

    /**
     * @return The session cleaner thread
     */
    private Thread createThread() {
        SessionCleaner cleaner = new SessionCleaner();
        cleaner.setSessionTimeout(timeoutInSeconds);
        cleaner.setPriority(Thread.MIN_PRIORITY);

        return cleaner;
    }

    Connection getConnection() throws SQLException {
        return dataSource.getConnection();
    }

    // -------------------------------------------------------------------------

    class SessionCleaner extends Thread {

        private int seconds;

        public void setSessionTimeout(int seconds) {
            this.seconds = seconds;
        }

        public void run() {
            while (!Thread.currentThread().isInterrupted()) {
                Connection conn = null;
                PreparedStatement ps = null;
                try {
                    conn = getConnection();

                    ps = conn.prepareStatement(SQL.DROP_OLD_SESSIONS);
                    ps.setInt(1, seconds / 60);
                    ps.executeUpdate();

                } catch (SQLException e) {
                    logger.error("Error during session cleaner", e);
                } finally {
                    SQL.close(ps);
                    SQL.close(conn);
                }

                // The session cleaner sleep the half of timeoutInSeconds in minutes
                try {
                    Thread.sleep((seconds * 1000) / 2);
                } catch (InterruptedException ignore) {
                }
            }
        }
    }
}
